package com.ar4j.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Various collection related utilities
 */
public class CollectionUtils {
  public static final Map<?, ?> EMPTY_MAP = Collections.unmodifiableMap(new HashMap<Object, Object>());
  public static final Set<?> EMPTY_SET = Collections.unmodifiableSet(new HashSet<Object>());
      
  private CollectionUtils() {
    // NO CONSTRUCTOR FOR YOU
  }
  
  /**
   * Adds an instance to a multi-map (a map of lists of a specific type), mapped by the given
   * key.
   * 
   * @param <E> type of instance the multi-map holds
   * @param id key of mapping
   * @param map multi-map instance
   * @param instance instance to add
   * @return the list within the multi-map to which the instance was added
   */
  public static <E, F> List<F> addToMapOfLists(E id, Map<E, List<F>> map, F instance) {
    List<F> list = map.get(id);
    if(list == null) {
      list = new ArrayList<F>();
      map.put(id, list);
    }
    
    list.add(instance);
    return list;
  }
  
  /**
   * Adds an instance to a multi-map (a map of sets of a specific type), mapped by the given
   * key.
   * 
   * @param <E> type of instance the multi-map holds
   * @param id key of mapping
   * @param map multi-map instance
   * @param instance instance to add
   * @return the set within the multi-map to which the instance was added
   */
  public static <E, F> Set<F> addToMapOfSets(E id, Map<E, Set<F>> map, F instance) {
    Set<F> list = map.get(id);
    if(list == null) {
      list = new LinkedHashSet<F>();
      map.put(id, list);
    }
    
    list.add(instance);
    return list;
  }
  
  /**
   * Adds an instance to a map-of-maps (a map of maps of a specified type), mapped by the
   * given first and second keys.
   * @param <E> type of first key
   * @param <F> type of second key
   * @param <G> type of instance stored
   * @param map main map-of-maps
   * @param firstKey first key to use
   * @param secondKey second key to use
   * @param instance instance to store
   * @return the inter map (from F to G) that the instance was added to
   */
  public static <E, F, G> Map<F, G> addToMapOfMaps(Map<E, Map<F, G>> map, E firstKey, F secondKey, G instance) {
    Map<F, G> subMap = map.get(firstKey);
    if(subMap == null) {
      subMap = new LinkedHashMap<F, G>();
      map.put(firstKey, subMap);
    }
    
    subMap.put(secondKey, instance);
    return subMap;
  }

  /**
   * Retrieves an element from a map-of-maps.
   * 
   * @param <E> type of first key
   * @param <F> type of second key
   * @param <G> type of instance stored
   * @param map main map-of-maps
   * @param firstKey first key to use
   * @param secondKey second key to use
   * @return the stored instance or null if none is found
   */
  public static <E, F, G> G getFromMapOfMaps(Map<E, Map<F, G>> map, E firstKey, F secondKey) {
    G out = null;
    Map<F, G> subMap = map.get(firstKey);
    if(subMap != null) {
      out = subMap.get(secondKey);
    }
    
    return out;
  }
  
  /**
   * Converts the result of a queryForList operation on a JdbcTemplate into a map. The result
   * is assumed to contain only unique values under the keyColumn.
   * 
   * Retrieves the mapping key from the result by finding it in the keyColumn and converting it to the
   * given keyClass. Retrieves the mapping value from the result by finding it in the valueColumn
   * and converting it to the valueClass.
   * 
   * Uses ClassUtils.coerseValueToType to perform type coercion for both key and value.
   */
  public static <E, F> Map<E, F> convertQueryForListResultToMap(List<Map<String, Object>> queryForListResult, String keyColumn, Class<E> keyClass, String valueColumn, Class<F> valueClass) {
    Map<E, F> out = new HashMap<E, F>();
    for(Map<String, Object> columnMap : queryForListResult) {
      Object keyObject = columnMap.get(keyColumn);
      Object valueObject = columnMap.get(valueColumn);
      E key = ClassUtils.coerceValueToType(keyObject, keyClass);
      F value = ClassUtils.coerceValueToType(valueObject, valueClass);
      
      if(out.containsKey(key)) {
        throw new IllegalArgumentException("Duplicate key found in result, key = " + key + ", value = " + value);
      } else {
        out.put(key, value);
      }
    }
    
    return out;
  }
  
  /**
   * Converts the result of a queryForList operation on a JdbcTemplate into a map of sets. The result
   * may contain non unique values under the keyColumn.
   * 
   * Retrieves the mapping key from the result by finding it in the keyColumn and converting it to the
   * given keyClass. Retrieves the set values from the result by finding it in the valueColumn
   * and converting it to the valueClass.
   * 
   * Uses ClassUtils.coerseValueToType to perform type coercion for both key and value.
   */
  public static <E, F> Map<E, Set<F>> convertQueryForListResultToMapOfSets(List<Map<String, Object>> queryForListResult, String keyColumn, Class<E> keyClass, String valueColumn, Class<F> valueClass) {
    Map<E, Set<F>> out = new HashMap<E, Set<F>>();
    for(Map<String, Object> columnMap : queryForListResult) {
      Object keyObject = columnMap.get(keyColumn);
      Object valueObject = columnMap.get(valueColumn);
      E key = ClassUtils.coerceValueToType(keyObject, keyClass);
      F value = ClassUtils.coerceValueToType(valueObject, valueClass);
      
      addToMapOfSets(key, out, value);
    }
    
    return out;
  }
  
  /**
   * Converts the result of the queryForList operation on a JdbcTemplate into a list of values. 
   * The values are extracted from each row in the result by finding the value of the given columnName
   * and coercing it to the requiredType.
   */
  public static <E> List<E> convertQueryForListResultToList(List<Map<String, Object>> queryForListResult, String columnName, Class<E> requiredType) {
    List<E> out = new ArrayList<E>();
    for(Map<String, Object> columnMap : queryForListResult) {
      Object columnObject = columnMap.get(columnName);
      E object = ClassUtils.coerceValueToType(columnObject, requiredType);
      out.add(object);
    }
    
    return out;
  }
  
  /**
   * Validates a collection parameter, usually to a bean setter, and throws an
   * IllegalArgumentException if the given parameter is null
   */
  public static void validateNonNullCollectionParameter(Collection<?> value, String name) {
    if(value == null) {
      throw new IllegalArgumentException(name + " collection parameter cannot be null");
    }
  }
  
  /**
   * Validates a map parameter, usually to a bean setter, and throws an
   * IllegalArgumentException if the given parameter is null
   */
  public static void validateNonNullMapParameter(Map<?, ?> value, String name) {
    if(value == null) {
      throw new IllegalArgumentException(name + " map parameter cannot be null");
    }
  }
  
  /**
   * @return true if collection is empty (null or empty)
   */
  public static boolean isEmptyCollection(Collection<?> c) {
    return c == null || c.size() == 0;
  }
  
  /**
   * @return a collection with the given value as its only element
   */
  public static <E> Collection<E> getSingleValueCollection(E value) {
    Collection<E> out = new ArrayList<E>();
    out.add(value);
    return out;
  }
  
  /**
   * @return a new map with a single mapping of the given key and value
   */
  public static <E, F> Map<E, F> getSingleValueMap(E key, F value) {
    Map<E, F> out = new HashMap<E, F>();
    out.put(key, value);
    return out;
  }
  
  /**
   * @return a new set with a single value 
   */
  public static <E> Set<E> getSingleValueSet(E value) {
    Set<E> out = new HashSet<E>();
    out.add(value);
    return out;
  }
  
}
